文章中讨论的内容基于以下版本的shiro:
1 | <dependency> |
参考资料
INI配置文件
自定义PermissionResolver
加密/解密
QuickStart
使用IniSecurityManagerFactory创建SecurityManager,会创建一个DefaultSecurityManager,DefaultSecurityManager里的Realm类是IniRealm.
IniSecurityManagerFactory已过时。可以使用新的方式创建SecurityManager:
1.创建一个DefaultSecurityManager
2.设置DefaultSecurityManager的Realm为IniRealm
1 | DefaultSecurityManager securityManager = new DefaultSecurityManager(); |
1 |
|
ini文件配置:
1.变量名 = 全限定类名会自动创建一个类实例,相对于调用public无参构造器创建对象
2.变量名.属性=值 自动调用相应的setter方法进行赋值,相当于调用setter方法设置常量值
3.$变量名 引用之前的一个对象实例,相当于调用setter方法设置对象引用
Principals/credentials
在shiro中,用户需要提供principals(身份)和credentials(证明)给shiro,从而应用能验证用户身份:
principals:
身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/密码/手机号。credentials:
证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
最常见的principals和credentials组合就是用户名/密码了。
身份认证
1.1 步骤
1.收集用户身份/凭证,比如用户名/密码
2.调用Subject.login进行登陆,如果认证失败,捕获AuthenticationException异常及其子类异样,返回错误信息给用户
3.调用Subject.logout进行退出操作
1.2 流程
1.首先调用Subject.login(token)进行登录,其会自动委托给SecurityManager
2.SecurityManager会委托给Authenticator进行身份验证,Authenticator才是真正的身份验证者,默认使用的Authenticator是ModularRealmAuthenticator,此处可以实现自定义的Authenticator
4.如果有多个Realm,Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证
5.Authenticator会把相应的token传入AuthenticatingRealm,最终调用AuthenticatingRealm类的getAuthenticationInfo方法进行身份|密码验证。
AuthenticatingRealm类的getAuthenticationInfo方法中有一个doGetAuthenticationInfo方法,它是完成身份认证
的方法。自定义的Realm类可以重写这个方法进行自定义的身份认证。完成身份认证后,doGetAuthenticationInfo方法返回AuthenticationInfo(比如SimpleAuthenticationInfo),AuthenticationInfo返回的信息包含了如何校验密码的信息,AuthenticatingRealm会调用assertCredentialsMatch(token,info)来完成密码校验
。
1 | public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { |
Realm
SecurityManager是管理认证和授权的,一个SecurityManager可以对应多个Realm类来实现认证和授权。
SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法,也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作。
在Shiro中,内置了几种Reaml类。默认使用的是IniRealm。
比如,在使用账号密码进行登陆的时候,会进入到AuthenticatingRealm类中,最终调用Realm类中的doGetAuthenticationInfo方法。
JdbcRealm
可以使用Shiro内置的JdbcRealm类到数据中查询,JdbcRealm中内置了sql语句,从sql语句中可以推出表的结构。
users表: 由username,password,password_salt字段组成。
如果需要使用JdbcRealm类:
1.需要导入数据库和表
2.需要配置DataSource和数据库连接信息
1 | dataSource = com.alibaba.druid.pool.DruidDataSource |
可以配置多个Realm类,使用逗号分隔
1 | securityManager.realms = $jdbcRealm,$myRealm |
自定义Realm类
报错:Ini instance and/or resourcePath resulted in null or empty Ini configuration. Cannot load account data.
在写测试类的时候,可能加入了@SpringbootTest,加了这个注解后springboot内置的SecurityManager就生效了,可能与自定义的IniSecurityManager冲突了。
自定义Realm类有以下方式:
1.1 实现Realm接口
实现接口的getAuthenticationInfo方法,需要自己实现密码的校验:
1 | public class MyRealm implements Realm { |
在ini配置文件中注入自定义的Realm类
1 | [main] |
1.2 继承AuthenticatingRealm
AuthenticatingRealm重写了getAuthenticationInfo方法,它会调用我们自定义实现的Realm类的doGetAuthenticationInfo方法来获取AuthenticationInfo信息。
AuthenticationInfo信息包括密码,加密盐值等。
之后AuthenticationRealm会调用assertCredentialsMatch来校验密码。
AuthenticatingRealm继承了CachingRealm,也具有缓存的功能。
与1.1中实现Realm的接口相比,继承AuthenticatingRealm接口无需我们去校验密码,我们只需要把密码,加密盐值等信息封装到AuthenticationInfo中即可。
1 | public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { |
实现自定义的Realm类:
1 | public class MyRealm extends AuthenticatingRealm { |
ini配置文件中配置使用的Realm类以及密码加密:
1 | # 密码加密 |
1.3 继承AuthorizingRealm
AuthenticatingRealm类不是Authorizer的实例,无法管理角色和权限,但是能管理登陆认证。
AuthorizatingRealm类是Authorizer的实例,在AuthenticatingRealm的基础上,可以管理角色和权限。
AuthorizatingRealm类具有缓存、身份认证、授权管理的功能,一般实现自定义的Realm类继承AuthorizingRealm类是比较合适的。
1 | public class MyRealm extends AuthorizingRealm { |
hasRole,isPermitte最终都是调用的Realm的doGetAuthorizationInfo来进行角色、权限的校验。
AuthenticationStrategy
如果配置了多个Realm类,有3种不同的认证策略:
1.AllSuccessfulStrategy: 所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了
2.AtLeastOneSuccessfulStrategy: 只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,返回所有Realm身份验证成功的认证信息
3.FirstSuccessfulStrategy: 只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略;
1 | authenticator = org.apache.shiro.authc.pam.ModularRealmAuthenticator |
如果报错No realms have been configured! One or more realms must be present
,则要把认证策略的配置优先于Realm类的配置。
授权
1.1 步骤
调用hasRole/isPermitted方法检查是否具有对应的角色/权限,执行不同的操作。
hasRole/hasAllRole: 判断用户是否拥有某个/所有角色,返回布尔值,不会抛出异常
checkRole/checkRoles: 判断用户是否拥有某个/所有角色,判断为假的时候会抛出UnauthorizationException
isPermitted/isPermittedAll: 判断用户是否拥有某个/所有权限,返回布尔值,不会抛出异常
checkPermission/checkPermissions: 判断用户是否拥有某个/所有权限,判断为假的时候会抛出UnauthorizationException
1.2 流程
1.首先调用Subject.isPermitted/hasRole*接口,其会委托给SecurityManager,而SecurityManager接着会委托给Authorizer
2.Authorizer是真正的授权者,默认的Authorizer是ModularRealmAuthorizer,如果我们调用如isPermitted(“user:view”),其首先会通过PermissionResolver(默认是WildcardPermissionResolver)把字符串转换成相应的Permission实例。如果我们调用如isRole(“user”),则不会解析成Permission实例,直接使用字符串进行判断。
3.在进行授权之前,其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限
4.Authorizer会判断Realm的角色/权限是否和传入的匹配,如果有多个Realm,会进行循环判断,如果匹配如isPermitted/hasRole*会返回true,否则返回false表示授权失败。
ModularRealmAuthorizer#hasRole:
1 | public boolean hasRole(PrincipalCollection principals, String roleIdentifier) { |
密码加密
1.1 Shiro提供的工具
Shiro提供了base64和16进制字符串编码/解码的API支持,方便一些编码解码操作。Shiro内部的一些数据的存储/表示都使用了base64和16进制字符串。
Base64
1 | // 加密 |
Hex
1 | String encodeToString = Hex.encodeToString("hello".getBytes(StandardCharsets.UTF_8)); |
SimpleHash
1 | Md5Hash md5Hash = new Md5Hash("1234", "salt", 2); |
DefaultHashService
1 | DefaultHashService defaultHashService = new DefaultHashService(); |
使用SHA-512加密时Ini文件的配置:
1 | sha512 = org.apache.shiro.authc.credential.Sha512CredentialsMatcher |
1.2 PasswordMatcher
Shiro提供了PasswordService及CredentialsMatcher用于提供加密密码及验证密码服务,不能提供自己的盐
。
使用DefaultPasswordService配合PasswordMatcher实现简单的密码加密与验证服务。
在保存密码到数据库时,使用PasswordService加密密码。在Realm类设置PasswordMatcher来验证密码服务。
1 |
|
1.3 HashedCredentialsMatcher
和之前的PasswordMatcher不同的是,它只用于密码验证,且可以提供自己的盐,而不是随机生成盐。
1 |
|
密码加盐
如果两个密码原文一样,生成的密文也是一样的。我们可以使用密码加盐的方式来处理。
如果使用的是JdbcRealm类,JdbcRealm提供了4种SaltType:
SaltStyle 含义
NO_SALT 默认,密码不加盐
CRYPT 密码是以Unix加密方式储存的
COLUMN salt是单独的一列储存在数据库中
EXTERNAL salt没有储存在数据库中,需要通过JdbcRealm.getSaltForUser(String)函数获取
在COLUMN这种情况下,SQL查询结果应该包含两列,第一列是密码,第二列是盐,严格区分顺序。
在JdbcRealm类中默认是使用字段password_salt来指定盐值的。我们也可以在ini文件中自定义
1 | # 加盐的类型 |
由于ini文件中不支持枚举类型,而SaltType的值是枚举类型的,我们需要一个转换器将String类型的值转换成枚举类型,绑定到JdbcRealm.SaltType上。
1 | // 将ini文件中的字符串配置值转换成SaltStyle的枚举类 |
springboot启动项目报错
Description:
Parameter 0 of method authorizationAttributeSourceAdvisor in org.apache.shiro.spring.boot.autoconfigure.ShiroAnnotationProcessorAutoConfiguration required a bean named ‘authenticator’ that could not be found.
Action:
Consider defining a bean named ‘authenticator’ in your configuration.
Springboot会自动注入一个DefaultWebSecurityManager.
而SecurityManger的创建过程需要依赖Authorizer.默认的情况下,如果我们不提供Authorizer,springboot是会自动创建这个Authorizer的bean的.
但是很巧的是,我们自己实现的Realm类需要继承AuthorizingRealm类,而AuthorizingRealm类实现了Authorizer接口
1 | public abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware { |
当springboot在创建SecurityManager时需要一个Authorizer,而一旦我们自己提供了Authorizer这个bean(Realm类继承了AuthorizingRealm类),springboot就不会再创建这个Authorizer了。
springboot在创建SecurityManager是通过代理去创建的,会去查找bean的名称叫做authorizer的bean,如果找不到,会出现了以上的错误。
解决办法:
1.将自定义的Realm类的bean名称设置为authorizer
2.在配置类中添加authorizer
1 | // bean的名称一定要叫authorizer |
3.不使用springboot自动配置注入的SecurityManager,自己注入一个SecurityManager
‘authenticator’ that could not be found
登陆页面
1.如果登陆页面放在template目录下,需要引入thymeleaf依赖。
如果页面是.html结尾的,后端控制器返回页面的时候可以带后缀,也可以不带后缀。
1 |
|
2.Shiro默认的登陆页面是login.jsp
如果指定了shiro.loginUrl之后,无需再对loginUrl配置anon的过滤器。
1 | shiro: |
如果后端跳转前端页面的url是login(GET方法),后端真正实现登陆逻辑的url是login(POST方法),
则在ShiroFilterChainDefinition配置的/login指的是POST方法的/login。
1 | defaultShiroFilterChainDefinition.addPathDefinition("/login", "authc"); |
3.前端使用Form表单的会导致进行两次认证请求。可以将input改成button,点击按钮的时候再提交表单。
1 | <button id="loginBtn">登陆系统</button> |
登出系统
如果不需要实现自定义的登出逻辑,直接使用Shrio提供的logout过滤器,会清除缓存后重定向到登陆页面。
可以查看LogoutFilter查看具体的细节。
1 | defaultShiroFilterChainDefinition.addPathDefinition("/logout", "logout"); |
如果需要自定义登出逻辑,自定义一个/logout的url实现登出逻辑。
如果配置了/logout是anon,在退出登陆的时候需要判断isAuthenticated()
如果配置/logout是authc,在退出登陆的时候无需判断isAuthenticated()
1 | "/logout") ( |
缓存管理
Shiro CacheManager
Shiro Caching官方文档
Shiro提供了类似于Spring的Cache 抽象,即Shiro本身不实现Cache,但是对Cache进行了又抽象,方便更换不同的底层Cache实现.
CacheManager是Shiro包中的一个接口,任意的数据源只要实现了这个接口,都可以嵌入到Shiro中.
CacheManager是一个容器,管理着Cache<K,V>,根据cacheName获取Cache<K,V>
Cache接口也是Shiro中的一个接口. Cache<K,V>是一个小容器.
在Shiro中,AuthenticationInfo、AuthorizationInfo会各自生成一个Cache,根据唯一的名字cacheName保存到CacheManager这个容器中.
1 | public interface CacheManager { |
如果在SecurityManager中设置了CacheManager,SecurityManager会自动检测相应的对象(如Realm)是否实现了CacheManagerAware接口并自动注入对应的CacheManager.
CachingRealm实现了CacheManagerAware接口,而AuthentingRealm | AuthorizatingRealm 都直接|间接实现了CacheManager.那么实现了CacheManagerAware的接口的Realm都会被设置这个CacheManager.
1.1 MemoryConstrainedCacheManager
MemoryConstrainedCacheManager是基于Map数据结构的缓存,它只适用于单个JVM的情况,而不适用于分布式的环境。
每个cacheName都对应着一个MapCache实例。
给SecurityManager添加一个CacheManager,同时开启Realm类的AuthenticationCache/AuthorizationCache.
1 |
|
1.2 EhCacheManager
使用EhCache需要导入相应的依赖:
1 | <dependency> |
修改CacheManager为EhCacheManager实现:
1 |
|
JWT + Shiro
JWT
JWT的格式: header(Base64) + payload(Base64) + siguature(加密算法)
header : 声明JWT的类型以及加密的算法
payload: 用户自定义的信息
siguature: Base64编码后的header + . + Base64编码后的payload进行加密
Base64可以被encode,相当于是明文,不能存储重要的信息,比如不能在payload中存储密码
JWT的验证: JWT的验证只是和加密的算法和使用的secert有关,与payload的自定义信息无关.
自定义Shiro过滤器
思路:
在用户使用login登陆的时候,不使用Shiro去进行身份认证.自定义方法验证用户名密码正确后,使用JWT给已经认证的用户颁发一个token.
当调用其他需要登陆后才能使用的接口时,使用颁发的token来请求其他的接口.
接口会通过自定义的Shiro Filter,在Filter中从request | request header中取出token,并使用getSubject.login方法来校验token并进行身份认证.
当token身份认证成功之后,如果是需要访问roles/permissions相关的接口时,会调用Realm的授权方法.
步骤:
1.实现自定义的AuthenticationToken,Realm进行身份认证时使用
2.实现自定义的Realm,对token进行校验和授权
3.实现自定义的Shiro Filter,拦截需要token的请求
4.关闭Shiro的session.JWT是无状态的,由于接入cas会和shiro的session管理冲突,所以关闭shiro的session,进行无状态管理.
1.实现自定义的AuthenticationToken,Realm中doGetAuthenticationInfo方法的参数
Credenticals可以传递空字符串,后面进行身份认证的时候返回SimpleAuthentincationInfo中的Credenticals也返回空字符串就可以了
1 |
|
2.实现自定义的Realm注意一定要实现supports方法
,Realm在进行身份认证的时候,如果不支持的support token是不会进行身份认证的.
doGetAuthenticationInfo的返回值SimpleAuthenticationInfo传递的Credentical会与token中的Credientical进行比较
在选择CredentialsMatcher时需要注意
doGetAuthorizationInfo的参数PrincipalCollection的值就是在doGetAuthenticationInfo的返回值SimpleAuthenticationInfo传递的.
1 | public class JWTRealm extends AuthorizingRealm { |
3.实现自定义的Shiro Filter
在Filter中进行请求的拦截并调用Subject.login方法委托给Realm进行身份认证和授权.
在Realm中抛出的异常可以在Subject.login处捕获,不能在全局异常处理中捕获.
Filter返回JSON的结果,使用Response用流输出到浏览器.
1 | public class JWTFilter extends AccessControlFilter { |
4.Shiro Config配置过滤器,session
session的关闭有以下几个步骤:
1.实现自定义的SubjectFactory,关闭session的创建
2.实现自定义的sessionManager,关闭session轮询校验
3.禁用session持久化
SubjectFactory:
1 | public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory { |
ShiroConfig:
这里的JwtRealm使用的CredenticalsMatcher是默认的SimpleCredenticatlsMatcher,它会将SimpleAuthenticationInfo的Credentical与doGetAuthenticationInfo方法的参数AuthenticaionInfo进行equals的比较.
1 |
|
5.JWT的工具类
使用@ConfigurationProperties从配置文件中注入成员属性.
静态成员属性的注入使用非static的setter方法注入.
1 |
|
application.yaml:
1 | jwt: |